Skip to content

数据录制 ​

6.1 带相机的遥操作 ​

bash
lerobot-teleoperate \
  --robot.type=so101_follower \
  --robot.port=/dev/tty.usbmodem5B8E1136841 \
  --robot.id=my_awesome_follower_arm \
  --teleop.type=so101_leader \
  --teleop.port=/dev/tty.usbmodem5B790188161 \
  --teleop.id=my_awesome_leader_arm \
  --robot.cameras="{wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, \
environment: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
  --display_data=true

6.2 录制数据集 ​

bash
lerobot-record \
  --robot.disable_torque_on_disconnect=true \
  --robot.type=so101_follower \
  --robot.port=/dev/tty.usbmodem5B8E1136841 \
  --robot.id=my_awesome_follower_arm \
  --teleop.type=so101_leader \
  --teleop.port=/dev/tty.usbmodem5B790188161 \
  --teleop.id=my_awesome_leader_arm \
  --robot.cameras="{wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, \
environment: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
  --dataset.repo_id=local/so101_demo \
  --dataset.root=/Users/xxx/project/lerebot/datasets/so101_demo \
  --dataset.single_task="Grab the black cube" \
  --dataset.num_episodes=30 \
  --dataset.episode_time_s=30 \
  --dataset.reset_time_s=20 \
  --dataset.push_to_hub=false \
  --display_data=true

本方案把数据固定写到项目的 datasets/ 下。注意 --dataset.root 指向的是数据集目录本身(源码里 Path(root) 直接用,不再拼 repo_id),所以一条数据集 = 一个文件夹:datasets/so101_demo/。父目录 datasets/ 可以预先建好,但 so101_demo/ 这个叶子目录首次录制时必须不存在(源码是 mkdir(exist_ok=False)),重录要先删掉它,或改用 --resume=true 续录。不写 --dataset.root 时才会落到默认的 ~/.cache/huggingface/lerobot/<repo_id>。

体量预估:参数是 30 条 × 30 秒、间隔复位 20 秒:总时长约 30 × (30 + 20) = 1500 秒 ≈ 25 分钟(不含调试和重录);总计约 30 × 30 × 30 = 27000 帧、两路相机,落盘大约几百 MB 量级。开始前确认磁盘空间,录制过程中别让 Mac 休眠。

先定标准再开录 ​

ACT 这类模仿学习策略,效果上限取决于数据,不取决于训练步数。所以录之前先把下面三条定下来——录完几十条才发现样本不够,只能推倒重来:

维度目标值为什么是这个数
总条数≥ 50 集样本太少的策略很容易变成"背下这条轨迹",动作幅度、起点稍变就失败
单个位置重复次数同一位置录约 10 次用来抵消人手的细微差异,让模型学到"这件事怎么做"而不是"这一条轨迹怎么走"
初始位置多样性≥ 5 个不同位置物体起点是抓取任务里最容易过拟合的变量,多样性基本等于泛化能力

三条乘起来正好是 50 集:5 个位置 × 每个 10 次。别把 50 集全录在同一个位置——loss 会降得很漂亮,真上手一换位置就崩。

落地两个小动作:物体摆放用胶带在地上标出 5 个位置,避免凭感觉摆;每录完一集在纸上打一个勾,配合键盘控制,条数和位置都能对上号。

6.3 record 参数参考 ​

下面是 RecordConfig 与 DatasetRecordConfig 的全部字段。标 必填 的漏一个就直接起不来。

参数默认作用
--robot.*—从臂:type / port / id / cameras / disable_torque_on_disconnect
--teleop.*—主臂:type / port / id(主臂一律用 --teleop.)
--dataset.repo_id必填数据集标识,必须写成「命名空间/名字」带一个斜杠;不上传时只决定本地目录名
--dataset.single_task必填任务描述文本,会写进每条样本;为空直接 ValueError
--dataset.num_episodes50一共录多少条 episode
--dataset.episode_time_s60每条 episode 的录制时长(秒),到点自动进入复位阶段
--dataset.reset_time_s60两条之间的复位时间(秒),这段时间不写入数据集,用来把物体摆回初始位
--dataset.fps30数据集采样帧率(录制循环频率),相机的 fps 不要低于它
--dataset.rootNone落盘位置;不填就用 HF_LEROBOT_HOME/repo_id(默认 ~/.cache/huggingface/lerobot)
--dataset.videoTrue把图像编码成 mp4;关掉则存 png,体积暴涨
--dataset.push_to_hubTrue本方案必须显式设 false,否则录完会自动上传
--dataset.private / --dataset.tagsFalse / None只在 push 到 Hub 时生效,本地录制用不到
--dataset.rename_map{}重命名观测键,本地录制一般不需要
--play_soundsTrue用语音播报状态("Recording episode N"、"Reset the environment"…)
--resume=truefalse续录:num_episodes 为新增条数(写法见 6.5)
--display_dataFalse实时可视化:弹 Rerun 窗口看画面与关节曲线(详见下方)

--display_data 到底是什么 ​

它是「实时可视化」开关,不参与录制,完全不影响落盘数据。打开后会额外弹出一个 Rerun 窗口,把这轮的画面和关节数值实时画出来;不加它(默认 False)就什么都不弹,数据照样录。

源码里它只做两件事:init_rerun(session_name="recording") 启动 Rerun 查看器,然后在每个控制循环里调一次 log_rerun_data(observation, action) 把数据推过去。窗口里能看到:

  • 两路相机画面——观测里的图像数组(CHW 转成 HWC)当成图片推过去
  • 每个关节的数值曲线——一维数组会拆成 observation.state_0…5 与 action.action_0…5,按时间轴画出来

所以它的实际用途是:遥操作时确认两路画面没接反(wrist 跟着末端动、environment 视角固定)、录制时盯住有没有哪帧画面卡住或某个关节突然跳变、评测时看模型每一步输出了什么动作。

两个细节:窗口是独立的应用窗口(由 rerun-sdk 提供,LeRobot 依赖里已带);源码默认把它限制在系统内存的 10%(环境变量 LEROBOT_RERUN_MEMORY_LIMIT 可调),所以录得很久时它会丢弃早期数据,属正常现象。程序退出时会自动 rerun_shutdown() 关掉。

6.4 录制键盘控制 ​

录制开始后终端会监听按键(由 init_keyboard_listener() 注册),不用等倒计时跑完:

按键行为
→ 右方向键提前结束当前阶段:这一条录够了就按它,不必等满 20 秒;在复位阶段按则提前结束复位
← 左方向键放弃并重录上一条 episode,录废了用这个
Esc立即停止整个录制;当前这条也会被保存(哪怕没录满),已保存的 episode 都保留
Ctrl+C强制中断:正在录的这条会被丢弃(不调用 save_episode()),已保存的保留;终端会打一串 KeyboardInterrupt 回溯,属正常

节奏建议:正式录制之前,先小批量试水:--dataset.num_episodes=2 --dataset.episode_time_s=10,确认数据能落盘、能再读回来,再跑完整参数。录完检查 datasets/so101_demo/meta/info.json 里的 total_episodes 是否等于预期;想重新录要先删掉 datasets/so101_demo/ 目录(首次录制要求它不存在)。

6.5 续录数据集 ​

录到一半中断、或想分几次录,用 --resume=true 在同一个数据集上接着录。它会加载 root 下已有的数据集并追加新 episode,而不是新建一个。

bash
lerobot-record \
  --robot.disable_torque_on_disconnect=true \
  --robot.type=so101_follower \
  --robot.port=/dev/tty.usbmodem5B8E1136841 \
  --robot.id=my_awesome_follower_arm \
  --teleop.type=so101_leader \
  --teleop.port=/dev/tty.usbmodem5B790188161 \
  --teleop.id=my_awesome_leader_arm \
  --robot.cameras="{wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}, \
environment: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
  --dataset.repo_id=local/so101_demo \
  --dataset.root=/Users/xxx/project/lerebot/datasets/so101_demo \
  --dataset.single_task="Grab the black cube" \
  --dataset.num_episodes=29 \
  --dataset.episode_time_s=30 \
  --dataset.reset_time_s=20 \
  --dataset.push_to_hub=false \
  --resume=true \
  --display_data=true

关键:--dataset.num_episodes 在 --resume=true 下表示本次还要新增几条,不是数据集总数。循环条件是 while recorded_episodes < num_episodes,计数从 0 起。所以「已经录了 1 条、目标总共 30 条」应该写 --dataset.num_episodes=29。

另外注意日志:Recording episode N 打印的是数据集当前总条数(源码 log_say(f"Recording episode {dataset.num_episodes}"),取的是 meta.total_episodes),不是"本次第几条"。续录时它天然带着存量,所以显示到 31 并不代表多录了一条。换算:日志最后的数字 = 开始前的条数 + num_episodes − 1。想知道本次真录了多少,比较前后 meta/info.json 的 total_episodes。

实战踩坑 · 录到一半相机掉线:

现象:录到第十几集时程序卡住,日志刷 Waiting for image writer to terminate...,然后抛错:

text
TimeoutError: Timed out waiting for frame from camera OpenCVCamera(8) after 200 ms. Read thread alive: True.

原因:机械臂运动时拉扯到了末端相机的连接处,USB 接触不稳定,取帧超时。括号里的 (8) 就是出问题那一路相机的索引,照着抓拍出来的图就能认出是哪台。

处理:重新插紧相机线,并且预留足够长度的线缆余量——正式开录前先手动走一遍最远的姿态,看线会不会被绷到。然后用 --resume=true 接着录,已保存的 episode 都在,不必从头再来。

这也是第 3 篇"相机直连、别串 hub"的另一面:末端那路线是全场唯一一直在动的线,它最脆弱。